<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
    <script type="text/javascript" src="dbscan.js"></script>
    <!-- Dropbox paths from source folder -
         home desktop: ../../../../Desktop/My Dropbox 
         X40 notebook: ../../../../Desktop/Dropbox
     -->
    <script type="text/javascript" src="../../../../Desktop/My Dropbox/edu/visualization/javascript/data/MCAS item responses 2010 grade 3 ELA Northampton.js"></script>
    <script type="text/javascript" src="../../../../Desktop/My Dropbox/edu/visualization/javascript/data/MCAS item responses 2010 grade 3 ELA Northampton correct.js"></script>
    <style type="text/css">
        body { font-family: "Lucida Console", Monaco, monospace }
        #distDiv { font-size: small }
        th.leftalign { text-align: left }
        td.show { vertical-align: top; padding: 20px }
        td.dist { text-align: right }
        col.odd { background-color: lightgray }
        tr.odd { background-color: lightgray }
    </style>
    <body>
        <form>
            <table>
                <tr><td><label>epsilon <input type="text" name="epsilonInput" value=5></label>
                <tr><td><label>minimum points per cluster <input type="text" name="minPtsInput" value=5></label>
                <tr><td><button type="button" onclick="analyze()">analyze</button>
            </table>
        </form>
        
        <table>
            <tr>
                <td class="show"><div id="clusterDiv"></div>
            <tr>
                <td class="show"><div id="assignmentDiv"></div>
            <tr>
                <td class="show"><div id="distDiv"></div>
        </table>
        <script type="text/javascript">

var dbscan;

function analyze() {
    var eps = document.getElementsByName('epsilonInput')[0].value;
    var MinPts = document.getElementsByName('minPtsInput')[0].value;
    var D = studentResponses.map(function(r) r[4]);
    D.unshift(correct_response_string);  // point 0 is correct response string
    dbscan = new DBSCAN(D, weightedDist, eps, MinPts, weightedInit);
    showDist(dbscan);
    dbscan.run();
    showClusters(dbscan);
    showAssignments(dbscan);
    console.log('ok');
}

function stringDist(p1, p2) {
    var dist = 0;
    var length = this.D[p1].length;
    for (var letter = 0; letter < length ; letter++)
        if (this.D[p1].charCodeAt(letter) != this.D[p2].charCodeAt(letter)) dist++;
    return dist;
}

function similarIncorrectDist(p1, p2) {
    // point 0 is correct response string
    // look just at multiple choice and short answer questions (one point)
    var dist = 0;
    var length = this.D[p1].length;
    var correct = this.D[0];
    for (var i = 0; i < length ; i++)
        if ('ABCD+1'.indexOf(correct[i]) != -1)
            if (this.D[p1].charCodeAt(i) != this.D[p2].charCodeAt(i)
                || this.D[p1].charCodeAt(i) != correct.charCodeAt(i)) dist++;
    return dist;
}

var distFactor =  {
    // In weightedDist, adjust distances by the following amounts, 
    // where negative numbers move the points closer together 
    // and positive numbers further apart.

    // Neither student answered
    neitherAnswered:            -20 ,

    // Only one student answered, incorrectly
    oneAnsweredIncorrectly:       0 ,

    // Only one student answered, correctly
    oneAnsweredCorrectly:        25 ,  // not used - treated as oneCorrect

    // The same correct answer
    sameCorrectAnswer:           -2 ,

    // The same incorrect answer
    sameIncorrectAnswer:        -25 ,

    // The same incorrect item (possibly the same answer, actual answers not reported for this item)
    sameIncorrectItem:          -20 ,

    // Different incorrect answers
    differentIncorrect:         -10 ,

    // One answer is correct, the other incorrect
    oneCorrect:                  25 ,
};

var mc_items = [];

function weightedInit() {
    mc_items = [];
    for (var i = 0; i < this.D[0].length; i++) {
        if ('+ABCD-'.indexOf(this.D[0][i]) != -1) mc_items.push(i);
    }
}

function weightedDist(p1, p2) {
    // Compute distance using weighted factors.
    var s1 = this.D[p1];
    var s2 = this.D[p2];
    var correct = this.D[0];

    // Assume the distance between p1 and p2 = 100,
    // then apply an adjustment from distFactor
    // for each item response.
    var dist = 100;

    var responses = mc_items.map(function(i) [s1[i], s2[i], correct[i]]);
    var adjustments = responses.map(weightedAdjustment);
    for (var i in adjustments) {
        dist += adjustments[i];
    }
    return dist;
}

function weightedAdjustment(set) {
    // Return a distance adjustment for a set of responses
    // r1, r2, and correct, each a single character.
    var r1 = set[0], r2 = set[1], correct = set[2];
    var adj = 0;
    if (r1 == correct || r2 == correct)
        if (r1 == r2)
            adj += distFactor.sameCorrectAnswer;
        else
            adj += distFactor.oneCorrect;
    else if (r1 == r2)
        if (r1 == '-')
            adj += distFactor.sameIncorrectItem;
        else if (r1 == ' ')
            adj += distFactor.neitherAnswered;
        else
            adj += distFactor.sameIncorrectAnswer;
    else
        if (r1 == ' ' || r2 == ' ')
            adj += distFactor.oneAnsweredIncorrectly;
        else
            adj += distFactor.differentIncorrect;
    return adj;
}

function showDist(dbscan) {
    var div = document.getElementById('distDiv');
    var existingChildren = div.childNodes;
    while (existingChildren.length > 0)
        div.removeChild(existingChildren[0]);
    var table = document.createElement('table');
    var caption = document.createElement('caption');
    caption.innerHTML = "distance matrix";
    table.appendChild(caption);
    var n = dbscan.D.length;
    for (col = 0; col < n; col++) {
        thisCol = document.createElement('col');
        thisCol.setAttribute('class', (col % 2 == 0) ? 'even' : 'odd');
        table.appendChild(thisCol);
    }
    var row = new Array(n);
    for (var i = 0; i < n; i++) {
        row[i] = document.createElement('tr');
        row[i].setAttribute('class', (i % 2 == 0) ? 'even' : 'odd');
        var cell = new Array(n);
        for (var j = 0; j < n; j++) {
            cell[j] = document.createElement('td');
            cell[j].setAttribute('class', 'dist');
            cell[j].innerHTML = (i <= j ? ' ' : dbscan.dist(i, j));
            row[i].appendChild(cell[j]);
        }
        table.appendChild(row[i]);
    }
    div.appendChild(table);
}

function showClusters(dbscan) {
    var div = document.getElementById('clusterDiv');
    var existingChildren = div.childNodes;
    while (existingChildren.length > 0)
        div.removeChild(existingChildren[0]);
    var table = document.createElement('table');

    var headerRow = document.createElement('tr');
    var header0 = document.createElement('th');
    header0.innerHTML = 'cluster';
    headerRow.appendChild(header0);
    var header1 = document.createElement('th');
    header1.innerHTML = 'size';
    headerRow.appendChild(header1);
    var header2 = document.createElement('th');
    header2.setAttribute('colspan', '0');
    header2.setAttribute('class', 'leftalign');
    header2.innerHTML = 'points';
    headerRow.appendChild(header2);
    table.appendChild(headerRow);

    var nClusters = dbscan.cluster.length;
    var row = new Array(nClusters);
    for (var cluster = 0; cluster < nClusters; cluster++) {
        row[cluster] = document.createElement('tr');
        var clusterSize = dbscan.cluster[cluster].length;
        var cell = new Array(clusterSize);
        var clusterCell = document.createElement('th');
        clusterCell.innerHTML = cluster;
        row[cluster].appendChild(clusterCell);
        var sizeCell = document.createElement('td');
        sizeCell.innerHTML = clusterSize;
        row[cluster].appendChild(sizeCell);
        for (var p = 0; p < clusterSize; p++) {
            cell[p] = document.createElement('td');
            cell[p].innerHTML = dbscan.cluster[cluster][p];
            row[cluster].appendChild(cell[p]);
        }
        table.appendChild(row[cluster]);
    }
    div.appendChild(table);
}

function showAssignments(dbscan) {
    var div = document.getElementById('assignmentDiv');
    var existingChildren = div.childNodes;
    while (existingChildren.length > 0)
        div.removeChild(existingChildren[0]);
    var table = document.createElement('table');

    var headerRow = document.createElement('tr');
    var col0 = document.createElement('th');
    col0.setAttribute('onclick', 'sortPoints()');
    col0.innerHTML = 'point';
    headerRow.appendChild(col0);
    var col1 = document.createElement('th');
    col1.setAttribute('onclick', 'sortClusters()');
    col1.innerHTML = 'cluster';
    headerRow.appendChild(col1);
    var col2 = document.createElement('th');
    col2.innerHTML = 'value';
    headerRow.appendChild(col2);
    var col3 = document.createElement('th');
    col3.innerHTML = 'distance from correct';
    headerRow.appendChild(col3);
    table.appendChild(headerRow);

    var n = dbscan.assigned.length;
    var row = new Array(n);
    for (var point = 0; point < n; point++) {
        row[point] = document.createElement('tr');
        row[point].setAttribute('name', 'point assignment row');
        var pointCell = document.createElement('td');
        pointCell.innerHTML = point;
        row[point].appendChild(pointCell);
        var clusterCell = document.createElement('td');
        clusterCell.innerHTML = dbscan.assigned[point] == -1 ? 'noise' : dbscan.assigned[point];
        row[point].appendChild(clusterCell);
        var valueCell = document.createElement('td');
        valueCell.innerHTML = dbscan.D[point];
        row[point].appendChild(valueCell);
        var offCell = document.createElement('td');
        offCell.innerHTML = dbscan.dist(point, 0);
        row[point].appendChild(offCell);
        table.appendChild(row[point]);
    }
    div.appendChild(table);
    var info = document.createElement('p');
    info.innerHTML = "(Click on <b>point</b> or <b>cluster</b> to resort the list.)";
    div.insertBefore(info, table);
}

function sortPoints() {
    var rows = document.getElementsByName('point assignment row');
    var iRow = 0;
    for (var p in dbscan.D) {
        var cell = rows[iRow].firstChild;
        cell.innerHTML = p;
        cell = cell.nextSibling;
        cell.innerHTML = (dbscan.assigned[p] == -1 ? 'noise' : dbscan.assigned[p]);
        cell = cell.nextSibling;
        cell.innerHTML = dbscan.dist(p, 0);
        iRow++;
    }
}

function sortClusters() {
    var rows = document.getElementsByName('point assignment row');
    var iRow = 0;
    for (var c in dbscan.cluster) {
        for (var p in dbscan.cluster[c].sort(function(a, b) a - b)) {
            var cell = rows[iRow].firstChild;
            cell.innerHTML = dbscan.cluster[c][p];
            cell = cell.nextSibling;
            cell.innerHTML = c;
            cell = cell.nextSibling;
            cell.innerHTML = dbscan.D[dbscan.cluster[c][p]];
            cell = cell.nextSibling;
            cell.innerHTML = dbscan.dist(dbscan.cluster[c][p], 0);
            iRow++;
        }
    }
    for (var p in dbscan.D) {
        if (dbscan.assigned[p] == -1) {
            var cell = rows[iRow].firstChild;
            cell.innerHTML = p;
            cell = cell.nextSibling;
            cell.innerHTML = 'noise';
            cell = cell.nextSibling;
            cell.innerHTML = dbscan.D[p];
            cell = cell.nextSibling;
            cell.innerHTML = dbscan.dist(p, 0);
            iRow++;
        }
    }
}

        </script>
    </body>
</html>
